package stv5;

import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map.Entry;

import org.apache.xerces.parsers.DOMParser;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import stv5.abstracted.Database;

public class FlatFileDatabase implements Database {	
	private String dbfile, newColor_S, oldColor_S; 
	private int lastId = 0; // the highest, assigned series ID	
	private HashMap<Integer, Series> seriesById;
	private LinkedList<Series> seriesOrdered;	
	private Document dbXmlDoc;
	
	public FlatFileDatabase(String dbfile) {
		this.dbfile = dbfile;
	}
	
	@Override
	public int createNewId() {
		return ++lastId;
	}		

	@Override
	public Collection<Series> getSeries() {
		return seriesOrdered;
	}	
	
	@Override
	public Series getSeries(int seriesId) {
		return seriesById.get(seriesId);
	}	
	
	@Override
	public boolean hasSeries(int seriesId) {
		return seriesById.containsKey(seriesId);
	}	
	
	@Override
	public boolean hasSeries(String folder) {
		for (Series s : seriesOrdered) {			
			if (s.getName().equalsIgnoreCase(folder)) 
				return true;		
		}		
		return false;
	}

	@Override
	public String loadLocalPlayer() {
		Node nodeVal = getSettingNode("local-player");		
		return nodeVal.getNodeValue();
	}

	@Override
	public int[] loadNewColor() {
		Node nodeVal = getSettingNode("new-color");		
		newColor_S = nodeVal.getNodeValue();
		return parseColorNode(nodeVal, DEFAULT_NEW_COLOR);
	}

	@Override
	public int[] loadOldColor() {
		Node nodeVal = getSettingNode("old-color");		
		oldColor_S = nodeVal.getNodeValue();
		return parseColorNode(nodeVal, DEFAULT_OLD_COLOR);
	}

	@Override
	public int loadOldDuration() {
		Node nodeVal = getSettingNode("old-duration");
		int dur = (nodeVal == null) ? DEFAULT_OLD_DURATION : Integer.parseInt(nodeVal.getNodeValue()); 
		return dur;
	}

	@Override
	public void rebuildDb(HashMap<String, String> seriesData, boolean deleteOld) {

		// update existing series
		String seriesName, guid;
		Series curr;
		Iterator<Series> iter = seriesOrdered.iterator();
		while(iter.hasNext()) {
			curr = iter.next();
			seriesName = curr.getName();
			
			// see if we want to just delete it
			if (deleteOld && !seriesData.containsKey(seriesName)) {
				iter.remove();
				continue;
			}
			
			guid = seriesData.get(seriesName);
			// if we have a new guid, update it and take it
			//  from the hashmap
			if (guid != null) {
				curr.setGuid(guid);
				seriesData.remove(seriesName);
			}
		}
		
		// now, simply insert anything remaining
		Series newSeries;
		for (Entry<String, String> e : seriesData.entrySet()) {
			newSeries = new Series(
					createNewId(),
					e.getKey(),
					e.getValue(),
					-1, 0
			);				
			
			// add it
			seriesById.put( newSeries.getId(), newSeries );
			seriesOrdered.add( newSeries );
		}
		
		// make sure it's actually sorted
		Collections.sort(seriesOrdered);
		
		// save!
		writeDbDoc();
	}

	@Override
	public Collection<Series> reloadSeries() {
		seriesById = new HashMap<Integer, Series>();
		seriesOrdered = new LinkedList<Series>();
				
		try {
			DOMParser parser = new DOMParser();
			parser.parse(dbfile);
			dbXmlDoc = parser.getDocument(); 			
			NodeList seriesNodes = dbXmlDoc.getElementsByTagName("series");
						
			NamedNodeMap currNodeAttributes;
			Series newSeries;
			for (int i=0; i<seriesNodes.getLength(); i++) { 
				currNodeAttributes = seriesNodes.item(i).getAttributes();
				newSeries = new Series(
						Integer.parseInt(currNodeAttributes.getNamedItem("id").getNodeValue()),
						currNodeAttributes.getNamedItem("name").getNodeValue(),
						currNodeAttributes.getNamedItem("guid").getNodeValue(),
						Integer.parseInt(currNodeAttributes.getNamedItem("lastplay").getNodeValue()),
						Integer.parseInt(currNodeAttributes.getNamedItem("lastview").getNodeValue())
				);				
				
				// add it
				seriesById.put( newSeries.getId(), newSeries );
				seriesOrdered.add( newSeries );
				
				// update "last id"
	            if (newSeries.getId() > lastId)
	            	lastId = newSeries.getId();		
			}
			Collections.sort(seriesOrdered);
		} catch (SAXException e) {
			System.out.println("\n - ERROR: Corrupt database. Consider deleting '"+dbfile+"' and " +
					"rerunning STv5 to create a valid, empty db.");
			System.exit(1);
		} catch (FileNotFoundException e) {
			System.out.println("\n - ERROR: Databasefile ("+dbfile+") not found. Preparing empty DB.");
			
			try {
				BufferedWriter out = new BufferedWriter(new FileWriter(dbfile));
				out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
				out.write("<stv5-db>\n");
				out.write(" <settings>\n");
				out.write("  <setting name=\"new-color\" value=\""+DEFAULT_NEW_COLOR_S+"\" />\n");
				out.write("  <setting name=\"old-color\" value=\""+DEFAULT_OLD_COLOR_S+"\" />\n");
				out.write("  <setting name=\"old-duration\" value=\""+DEFAULT_OLD_DURATION+"\" />\n");
				out.write("  <setting name=\"local-player\" value=\"\" />\n");
				out.write(" </settings>\n");
				out.write(" <series-list>\n </series-list>\n");
				out.write("</stv5-db>");
				out.close();
			} catch (IOException e1) {
				e1.printStackTrace();
				System.out.println(" - ERROR: Could not write to '"+dbfile+"'");			
			}		
			
			System.exit(1);
		} catch (IOException e) {
			e.printStackTrace();
			System.exit(1);
		} 
		
		return seriesOrdered;
	}

	@Override
	public void updateSeries(Series currSeries) {
		// the series itself is already updated at this point
		writeDbDoc();
	}
	
	protected void writeDbDoc() {
		try {
			BufferedWriter out = new BufferedWriter(new FileWriter(dbfile));
			//print(dbXmlDoc, out, false);			
			out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
			out.write("<stv5-db>\n");
			out.write(" <settings>\n");
			out.write("  <setting name=\"new-color\" value=\""+newColor_S+"\" />\n");
			out.write("  <setting name=\"old-color\" value=\""+oldColor_S+"\" />\n");
			out.write("  <setting name=\"old-duration\" value=\""+loadOldDuration()+"\" />\n");
			out.write("  <setting name=\"local-player\" value=\""+
					normalize(loadLocalPlayer())+"\" />\n");
			out.write(" </settings>\n");
			out.write(" <series-list>\n");
			
			// make sure it's ordered
			Collections.sort(seriesOrdered);
			
			// write the series
			for (Series s : seriesOrdered)
				out.write("  <series guid=\"" + normalize(s.getGuid()) +"\" " +
						"id=\"" + s.getId() + "\" " +
						"lastplay=\"" + s.getLastPlay() + "\" " +
						"lastview=\"" + s.getLastView() + "\" "+
						"name=\"" + s.getName() + "\" " +
						"/>\n");
			out.write(" </series-list>\n");
			out.write("</stv5-db>");
			out.close();
		} catch (IOException e) {
			e.printStackTrace();
		}		
	}	
	
	/**
	 * Return the Node with whose "name" is settingName
	 * @param settingName
	 * @return
	 */
    private Node getSettingNode(String settingName) {
    	NodeList settings = dbXmlDoc.getElementsByTagName("setting");
    	Node curr;
    	for (int i=0; i<settings.getLength(); i++) {
    		curr = settings.item(i);
    		if (curr.getAttributes().getNamedItem("name").getNodeValue()
    				.equals(settingName))
    			return curr.getAttributes().getNamedItem("value");
    	}
    	return null;
    }
    
    /**
     * Parse the value of a node to get its color value
     * 	from a hex string input (eg: cc9900)
     * 
     * @param theNode
     * @param defaultVal
     * @return
     */
    private int[] parseColorNode(Node theNode, int...defaultVal) {
    	String raw = theNode.getNodeValue();    	
    	int[] ret = defaultVal;
    	try {
    	ret[0] = Integer.parseInt(raw.substring(0, 2), 16);
		ret[1] = Integer.parseInt(raw.substring(2, 4), 16);
		ret[2] = Integer.parseInt(raw.substring(4, 6), 16);
    	} catch (Exception e) { 
    		ret = defaultVal;
    	}
		return ret;
    }	
	
	/*
	 * The following is taken (and modified) from the DOMWriter sample
	 *  Found at:
	 * http://svn.apache.org/viewvc/xerces/java/branches/xerces_j_1/samples/dom/DOMWriter.java?view=markup
	 */
	
      /** Normalizes the given string. */
    protected String normalize(String s) {
        StringBuffer str = new StringBuffer();

        int len = (s != null) ? s.length() : 0;
        for ( int i = 0; i < len; i++ ) {
            char ch = s.charAt(i);
            switch ( ch ) {
            case '<': {
                    str.append("&lt;");
                    break;
                }
            case '>': {
                    str.append("&gt;");
                    break;
                }
            case '&': {
                    str.append("&amp;");
                    break;
                }
            case '"': {
                    str.append("&quot;");
                    break;
                }
            case '\'': {
                    str.append("&apos;");
                    break;
                }
            /*
            case '\r':
            case '\n': {
                    if ( canonical ) {
                        str.append("&#");
                        str.append(Integer.toString(ch));
                        str.append(';');
                        break;
                    }
                    // else, default append char
                }*/
            default: {
                    str.append(ch);
                }
            }
        }

        return(str.toString());

    } // normalize(String):String
}
